package com.github.live.plugins;

import android.text.TextUtils;

import com.github.live.util.LogFactory;
import com.github.livebase.plugins.MetaData;
import com.github.livebase.plugins.PluginFactory;
import com.github.livebase.plugins.PluginContext;
import com.github.livebase.plugins.PluginException;
import com.github.livebase.plugins.Register;
import com.github.livebase.plugins.RegisterInfo;
import com.github.livebase.plugins.RegisterMataData;
import com.github.livebase.util.FileUtil;
import com.github.livebase.util.LiveContext;
import com.github.livebase.util.base.ArraysUtil;
import com.github.livebase.util.base.IOUtil;
import com.github.livebase.util.base.ObjectsUtil;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import dalvik.system.DexClassLoader;

/**
 * @ClassName C
 * @description:
 * @author: biao532
 * @create: 2023-12-20 15:16
 * @Version 1.0
 **/
public class PluginsLoader {
    private final int version = 1;
    private LiveContext mContext;
    private PluginFactory factory;
    public PluginsLoader(LiveContext context, PluginFactory pluginFactory) {
        this.mContext = context;
        this.factory = pluginFactory;
    }

    public void loadNet(InputStream stream) throws IOException, PluginException {
        File cache = new File(mContext.getExternalFilesDir("user_temp"), "plugin.zip");
        try (FileOutputStream out = new FileOutputStream(cache)) {
            IOUtil.transferTo(stream, out);
            load(cache.getParentFile());
        } catch (IOException | PluginException e) {
            LogFactory.warn(PluginsLoader.class, e,"an error occurred during load from net");
        }
        cache.delete();
    }

    // 从本地加载
    public void load(File dir) throws PluginException {
        File[] zipFiles = dir.listFiles((f)-> f.isFile() && ( f.getName().endsWith(".zip") || f.getName().endsWith(".zip.link")));
        ArraysUtil.sort(zipFiles, File::compareTo);
        for (File file : zipFiles) {
            try {
                if (file.getName().endsWith(".zip.link")) {
                    file = FileUtil.link(file, this.mContext);
                }
                ZipFile zipFile = new ZipFile(file);
                loadPlugin(zipFile);
            } catch (IOException e) {
                LogFactory.warn(PluginsLoader.class, e,"an error occurred during load from file {}", file);
            } catch (PluginException e) {
                throw e;
            }
        }
    }

    public void loadPlugin(ZipFile file) throws PluginException {
        MetaData properties = new MetaData(version, file);

        chkData(properties);
        File dir = unzipPlugin(properties, file);
        File path = getDexFile(properties, dir);
        // jar/apk路径  释放目录
        ClassLoader loader = new DexClassLoader(path.getAbsolutePath(),
                factory.getPluginsCacheDir().getAbsolutePath(),
                null, this.getClass().getClassLoader());

        PluginContext pluginContext = new PluginContext(properties.getName(), properties.getPluginVersion(), dir, properties.getConfigDir(), factory, loader);
        List<RegisterInfo> infos = loadRegister(loader, properties, pluginContext);
        factory.register(infos);
    }

    private List<RegisterInfo> loadRegister(ClassLoader classLoader, MetaData properties, PluginContext context) throws PluginException {
        Map<String, List<String>> map = properties.getRegisters();
        if (map == null || map.isEmpty()) {
            return ObjectsUtil.Empty.LIST;
        }
        List<RegisterInfo> list = new ArrayList<>();
        for (String key : map.keySet()) {
            for (String rs : map.get(key)) {
                LogFactory.debug(PluginsLoader.class, "found key[{}] value[{}] from plugin[{}]", key, rs, properties.getName());
                RegisterInfo r = loadRegister(classLoader, rs);
                if (r != null) {
                    r.setContext(context);
                    r.setKey(key);
                    list.add(r);
                }
            }
        }
        return list;
    }
    private RegisterInfo loadRegister(ClassLoader classLoader,  String classname) throws PluginException {
        try {
            Class<?> clazz = classLoader.loadClass(classname);
            if (!Register.class.isAssignableFrom(clazz)) {
                LogFactory.info(PluginsLoader.class, "{} is not subclass of {}", Register.class, classname);
                return null;
            }
            RegisterInfo info = new RegisterInfo(clazz);
            RegisterMataData md = clazz.getAnnotation(RegisterMataData.class);
            if (md != null) {
                if (!ArraysUtil.isEmpty(md.busEvent())) {
                    info.setBusEvent(md.busEvent());
                }
                info.setOnlyListener(md.onlyListener());
            }
            return info;
        } catch (ClassNotFoundException e) {
            throw new PluginException("class[classname=%s] not found", classname);
        }
    }

    private void chkData(MetaData properties) throws PluginException {
        String name = properties.getName();
        String dexPath = properties.getDex();
        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(dexPath)) {
            throw new PluginException("name or dex is empty, please check plugin");
        }
        if (properties.getVersion() > this.version) {
            throw new PluginException("plugin version is %d, the live support version <= %d", properties.getVersion(), this.version);
        }
    }
    private File unzipPlugin(MetaData properties, ZipFile file) throws PluginException {
        Enumeration<? extends ZipEntry> entries = file.entries();

        File tmp = new File(mContext.getExternalFilesDir("plugins"), properties.getName());
        if (!tmp.exists()) {
            if (!tmp.mkdirs()) {
                throw new PluginException("no such location path[%s] to unzip the plugin[name=%s]", tmp.getAbsolutePath(), properties.getName());
            }
        }
        String stats = properties.getConfigDir();
        File staticDir = null;
        if (!TextUtils.isEmpty(stats)) {
            staticDir = new File(tmp, stats);
        }
        clearDir(tmp, staticDir);

        while (entries.hasMoreElements()) {
            ZipEntry entry = entries.nextElement();
            writeToFileSystem(file, entry, tmp, staticDir);
        }
        return tmp;
    }
    private void clearDir(File dir, File exclude) {
        if (dir.isFile()) {
            return;
        }
        for (File file : dir.listFiles()) {
            if (file.isDirectory() && (exclude != null && !file.equals(exclude))) {
                clearDir(file, exclude);
            }
            file.delete();
        }
    }

    private boolean isOrInDir(File dir, File file) {
        while (file != null && !file.equals(dir)) {
            file = file.getParentFile();
        }
        return file != null;
    }
    private void writeToFileSystem(ZipFile file, ZipEntry entry, File dir, File staticDir) throws PluginException {
        File c = new File(dir, entry.getName());
        if (staticDir != null && isOrInDir(staticDir, c) && c.exists()) {
            return;
        }

        if (entry.isDirectory() && !c.exists()) {
            c.mkdirs();
            return;
        }

        File parent = c.getParentFile();
        if (!parent.exists()) {
            parent.mkdirs();
        }

        OutputStream outputStream = null;
        InputStream inputstream = null;
        try {
            inputstream = file.getInputStream(entry);
            if (!c.exists()) {
                c.createNewFile();
            }
            outputStream = new FileOutputStream(c);
            IOUtil.transferTo(inputstream, outputStream);
        } catch (IOException e) {
            throw new PluginException(e, "file[%s] dump failed", c.getAbsolutePath());
        } finally {
            if (inputstream != null) {
                try {
                    inputstream.close();
                } catch (IOException e) {
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                }
            }
        }
    }
    private File getDexFile(MetaData properties, File dir) {
        String dexPath = properties.getDex();
        return new File(dir, dexPath);
    }
}
